﻿using System;
using System.Collections;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Xml.Serialization;
using Mono.Cecil;
using YamlDotNet.RepresentationModel;

namespace YamltoMssql
{
    class Program
    {
        const System.Reflection.MethodAttributes GET_SET_ATTR = System.Reflection.MethodAttributes.Public | System.Reflection.MethodAttributes.SpecialName | System.Reflection.MethodAttributes.HideBySig;
        const string ASSEMBLY_NAME = "yamltypes";
        const string CONFIG_FILE = "mapping.config";
        const string SCRIPT_DIR = ".\\scripts";
        const string YAML_DIR = ".\\yaml";

        static readonly ConstructorInfo _columnAttributeConstructor = typeof(ColumnAttribute).GetConstructor(new Type[] { typeof(string), typeof(string), typeof(bool) });

        static void Main(string[] args)
        {
            var st = new Stopwatch();
            st.Start();

            bool forceBuild = true;

            var configPath = ".\\" + CONFIG_FILE;
            Mapping conf = ReadMapping(configPath);
            if(conf.Tables == null)
                throw new InvalidOperationException("Mapping not defined");

            /* Dynamic assembly generation */

            var asmPath = ".\\" + ASSEMBLY_NAME + ".dll";
            if (forceBuild  || ShouldBuildAssembly(asmPath, conf))
            {
                BuildMappingAssembly(conf);
                SaveMapping(conf,configPath);
            }

            var asm = Assembly.LoadFile( Path.GetFullPath(asmPath) );
            if (asm == null)
                throw new InvalidOperationException("Dynamic types assembly not loaded");

            var types = asm.GetExportedTypes();


            /* YAML file parsing */

            Console.WriteLine("Loading YAML files");

            var values = new ArrayList();
            if(Directory.Exists(Path.GetFullPath(YAML_DIR)))
                for(int i =0; i < types.Length; i++)
                {
                    if (!Attribute.IsDefined(types[i], typeof(MappingClassAttribute))) 
                        continue;
                    
                    string yamlfile = string.Format("{0}\\{1}.yaml",YAML_DIR, types[i].Name);
                    if (!File.Exists(yamlfile)) continue;

                    var yaml = new YamlStream();
                    yaml.Load(new StringReader(File.ReadAllText(yamlfile)));

                    var root = (YamlMappingNode)yaml.Documents[0].RootNode;
                    foreach (var node in root.Children)
                    {
                        var instance = Activator.CreateInstance(types[i]);

                        var pinfo = types[i].GetProperty("key");
                        pinfo.SetValue(instance, GetValue(((YamlScalarNode)node.Key).Value, pinfo.PropertyType), null);

                        foreach (var yamlNode in ((YamlMappingNode)node.Value).Children)
                        {
                            pinfo = types[i].GetProperty(((YamlScalarNode)yamlNode.Key).Value);
                            if (pinfo != null)
                            {
                                string value = ((YamlScalarNode)yamlNode.Value).Value;
                                if (string.IsNullOrEmpty(value)) continue;
                                pinfo.SetValue(instance, GetValue(value, pinfo.PropertyType), null);
                            }
                        }
                        values.Add(instance);
                    }
                }
            
            /* SQL script generation */

            var sb = new StringBuilder();
            sb.AppendLine(string.Format("/* Date : {0} - Version : {1} */", DateTime.Now, conf.Version));
            foreach (var value in values)
            {
                var type = value.GetType();
                var typemapping = conf.Tables.FirstOrDefault(t => String.Equals(t.Table, type.Name));
                if(string.IsNullOrEmpty(typemapping.Table) ||  typemapping.Columns == null) continue;

                var pk = typemapping.Columns.FirstOrDefault(c => c.PropertyName == "key");
                var pkvalue = type.GetProperty(pk.PropertyName).GetValue(value, null);

                sb.AppendLine(string.Format("IF NOT EXISTS (SELECT 1 FROM [{0}] Where [{1}] = {2})", typemapping.Table, pk.ColumnName, pkvalue is string ? "'" + ((string)pkvalue).Replace("'","''") + "'" : pkvalue ));
                sb.Append("INSERT INTO " + typemapping.Table + "(");
                sb.Append(string.Join(",", typemapping.Columns.Select(c => c.ColumnName)));
                sb.Append(")VALUES(");
                foreach (var column in typemapping.Columns)
                {
                    var pinfo = type.GetProperty(column.PropertyName);
                    if (pinfo != null)
                    {
                        var colatt = pinfo.GetCustomAttributes(typeof(ColumnAttribute), false)[0] as ColumnAttribute;
                        bool isString = pinfo.PropertyType == typeof(string);

                        var val = pinfo.GetValue(value, null);
                        if (val == null)
                        {
                            sb.Append(colatt.IsNullable ? "null," :  isString ? "''," : string.Format("{0},", Activator.CreateInstance(pinfo.PropertyType)));
                            continue;
                        }
                        
                        if (isString)
                            sb.Append(string.Format("'{0}',", ((string)val).Replace("'", "''")));
                        else
                            sb.Append((val is Boolean ? ((bool)val) ? "1" : "0" : val )+ ",");
                    }
                }
                sb.Remove(sb.Length - 1, 1);
                sb.Append(")");
                sb.AppendLine();
            }

            if (!Directory.Exists(Path.GetFullPath(SCRIPT_DIR)))
                Directory.CreateDirectory(Path.GetFullPath(SCRIPT_DIR));

            Console.WriteLine("Generating script files");
            using (var fs = new StreamWriter(string.Format("{0}\\Data.sql", Path.GetFullPath(SCRIPT_DIR)), false, Encoding.UTF8))
            {
                fs.Write(sb.ToString());
                fs.Flush();
            }
            sb.Clear();

            Console.WriteLine("Generating schema creation script");
            using (var fs = new StreamWriter(string.Format("{0}\\Schema.sql", Path.GetFullPath(SCRIPT_DIR)), false, Encoding.UTF8))
            {
                fs.Write(GenerateCreateTableScript(conf));
                fs.Flush();
            }

            st.Stop();
            Console.WriteLine("Generation finished. Time elapsed: {0}\r\nPress a key to exit",st.Elapsed);
            Console.ReadKey();
        }

        static bool ShouldBuildAssembly(string file, Mapping conf)
        {
            if(string.IsNullOrEmpty(file))throw new ArgumentNullException("file");
            if (!File.Exists(file)) return true;
            var def = AssemblyDefinition.ReadAssembly(file, new ReaderParameters(ReadingMode.Immediate));
            return def ==null || def.Name ==null || def.Name.Version ==null || def.Name.Version.Major < conf.Version;
        }
        
        static void BuildMappingAssembly(Mapping conf)
        {
            Console.WriteLine("Building dynamic assembly");

            var asmName = new AssemblyName(ASSEMBLY_NAME)
            {
                Version = new Version(conf.Version, 0),
                CultureInfo = CultureInfo.InvariantCulture
            };

            var asmBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Save);
            asmBuilder.DefineVersionInfoResource(ASSEMBLY_NAME, string.Format(CultureInfo.InvariantCulture, "{0}.0.0.0", conf.Version), "", "", "");

            var attCtor = typeof(AssemblyTitleAttribute).GetConstructor(new[] { typeof(string) });
            asmBuilder.SetCustomAttribute(new CustomAttributeBuilder(attCtor, new object[] { "Dynamic types mapping assembly" }));

            var typeAttributeCtor = typeof(MappingClassAttribute).GetConstructor(Type.EmptyTypes);

            var moduleBuilder = asmBuilder.DefineDynamicModule(asmName.Name, asmName.Name + ".dll");
            foreach (var tableMapping in conf.Tables)
            {
                var typeBuilder = moduleBuilder.DefineType(tableMapping.Table, System.Reflection.TypeAttributes.Public);
                foreach (var mapping in tableMapping.Columns)
                    CreateProperty(mapping.PropertyName, mapping.NativeType, typeBuilder,mapping);

                typeBuilder.SetCustomAttribute(new CustomAttributeBuilder(typeAttributeCtor, Type.EmptyTypes));
                typeBuilder.CreateType();
            }
            asmBuilder.Save(asmName.Name + ".dll");
        }

        
        static void CreateProperty(string propertyName, Type propertyType, TypeBuilder builder, ColumnMapping mapping)
        {
            var fb = builder.DefineField("_" + propertyName.ToLowerInvariant(), propertyType, System.Reflection.FieldAttributes.Private);

            var getMb = builder.DefineMethod(string.Format("get_{0}", propertyName), GET_SET_ATTR, propertyType, null);
            var getIlGen = getMb.GetILGenerator();
            getIlGen.Emit(OpCodes.Ldarg_0);
            getIlGen.Emit(OpCodes.Ldfld, fb);
            getIlGen.Emit(OpCodes.Ret);

            var setMb = builder.DefineMethod(string.Format("set_{0}", propertyName), GET_SET_ATTR, null, new Type[] { propertyType });
            var setIlGen = setMb.GetILGenerator();
            setIlGen.Emit(OpCodes.Ldarg_0);
            setIlGen.Emit(OpCodes.Ldarg_1);
            setIlGen.Emit(OpCodes.Stfld, fb);
            setIlGen.Emit(OpCodes.Ret);

            var pb = builder.DefineProperty(propertyName, System.Reflection.PropertyAttributes.None, propertyType, null);
            pb.SetGetMethod(getMb);
            pb.SetSetMethod(setMb);
            pb.SetCustomAttribute(new CustomAttributeBuilder(_columnAttributeConstructor, new object[] { mapping.ColumnName, mapping.Type, mapping.IsNullable }));
        }

        static object GetValue(string value, Type target)
        {
            if (string.IsNullOrEmpty(value)) return null;
            if (target == typeof(string) || target.IsAssignableFrom(typeof(string))) return value;
            switch (target.Name)
            {
                case "Int32":
                case "Int16":
                    return int.Parse(value);
                case "Boolean":
                    return bool.Parse(value);
                default:
                    return null;
            }
        }

        static Mapping ReadMapping(string file)
        {
            Console.WriteLine("Loading mapping configuration file");
            if(string.IsNullOrEmpty(file))throw new ArgumentNullException("file");
            if(!File.Exists(file))
            {
                var conf = new Mapping() { Version = 1 };
                SaveMapping(conf, file);
                return conf;
            }

            var xmlSerialiser = new XmlSerializer(typeof(Mapping));
            using (var stream = new FileStream(file, FileMode.Open, FileAccess.Read))
                return (Mapping)xmlSerialiser.Deserialize(stream);
        }

        static void SaveMapping(Mapping mapping, string file)
        {
            Console.WriteLine("Saving mapping configuration file");
            if (string.IsNullOrEmpty(file)) throw new ArgumentNullException("file");

            var xmlSerialiser = new XmlSerializer(typeof(Mapping));
            using (var stream = new FileStream(file, FileMode.Create, FileAccess.Write))
                xmlSerialiser.Serialize(stream, mapping);
        }

        static string GenerateCreateTableScript(Mapping conf)
        {
            var builder = new StringBuilder();
            builder.AppendLine(string.Format("/* Date : {0} - Version : {1} */", DateTime.Now, conf.Version));
            foreach (var table in conf.Tables)
            {
                builder.AppendLine(string.Format("/* {0} table creation */", table.Table));
                builder.Append(string.Format("IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{0}') AND type in (N'U')) ",table.Table));
                builder.AppendLine();
                builder.Append("Create table [" + table.Table + "] (");
                foreach (var column in table.Columns)
                {
                    builder.Append(string.Format("[{0}] {1} {2},", column.ColumnName, GetSqlType(column.NativeType), column.IsNullable && column.PropertyName != "key" ? "NULL" : "NOT NULL"));
                }
                var pk = table.Columns.FirstOrDefault(c => c.PropertyName == "key");
                if(!string.IsNullOrEmpty(pk.ColumnName))
                {
                    builder.Append(string.Format("CONSTRAINT [PK_{0}] PRIMARY KEY CLUSTERED ( [{1}] ASC )", table.Table, pk.ColumnName));
                }
                builder.Append(")");
                builder.AppendLine();
            }
            return builder.ToString();
        }

        static string GetSqlType(Type nativeType, int maxLength = 255)
        {
            var typename = nativeType.Name;
            switch (typename)
            {
                case "Int32":
                case "Int16":
                    return "int";
                case "Boolean":
                    return "bit";
                default:
                    return "varchar(" + maxLength + ")";
            }
        }
    }
    
}
